﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.SessionState;
using System.IO;
using System.Threading;
using System.Collections.Specialized;
using System.Reflection;

namespace TA.Turbo
{
	/// <summary>
	/// Processes the request and reads the posted files.
	/// </summary>
	public class FileUploadOperation
	{
		#region STATIC
		/// <summary>
		/// Gets the <see cref="FileUploadOperation"/> for the specified token.
		/// </summary>
		/// <remarks>
		/// The returned object can be used to query the status of the upload.
		/// </remarks>
		/// <param name="context"></param>
		/// <param name="token"></param>
		/// <returns></returns>
		public static FileUploadOperation Get(HttpContext context, string token)
		{
			return context.Application[ApplicationKeyPrefix + token] as FileUploadOperation;
		}
		internal static void Remove(HttpContext context, FileUploadOperation oper)
		{
			context.Application.Remove(ApplicationKeyPrefix + oper._token);
		}
		#endregion
		#region PUBLIC
		#region INIT
		internal FileUploadOperation(HttpContext context, HttpWorkerRequest wr)
		{
			this._context = context;
			this._wr = wr;
			this._token = context.Request.QueryString["t"];

			this._context.SetOperation(this);
			if (!string.IsNullOrEmpty(this._token))
			{
				this._context.Application[ApplicationKeyPrefix + this._token] = this;
			}
		}
		#endregion
		#region PROPS
		/// <summary>
		/// Gets the array of files that were uploaded so far.
		/// </summary>
		public UploadedFile[] Files
		{
			get
			{
				return this._files.ToArray();
			}
		}
		/// <summary>
		/// Gets the token of this operation.
		/// </summary>
		public string Token
		{
			get
			{
				return this._token;
			}
		}
		/// <summary>
		/// Gets the progress of this operation.
		/// </summary>
		public FileUploadProgress Progress
		{
			get
			{
				lock (this._progress)
				{
					this._progress._totalUploaded = this._stream.Position;
				}
				return this._progress;
			}
		}
		#endregion
		#region METHODS
		#endregion
		#endregion
		#region INTERNAL
		#region CONST/STATIC
		internal const string ApplicationKeyPrefix = "TA.Turbo.FileUpload";
		static readonly Type _tHttpValueCollection = Type.GetType("System.Web.HttpValueCollection, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
		static readonly ConstructorInfo _ctorHttpValueCollection = 
			_tHttpValueCollection.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, CallingConventions.Any, new Type[0], null);
		static readonly FieldInfo _f_form = typeof(HttpRequest).GetField("_form", BindingFlags.Instance | BindingFlags.NonPublic);
		#endregion
		#region VARS
		#region REQUEST
		HttpContext _context;
		HttpWorkerRequest _wr;
		FileUploadRequestStream _stream;
		string _token;
		#endregion
		#region READER
		FileUploadRequestReader _reader;
		#endregion
		#region BOUNDARY/HEADERS
		string _boundary;
		byte[] _boundaryBytes;
		List<Header> _headers = new List<Header>();
		NameValueCollection _nvcForm = (NameValueCollection)_ctorHttpValueCollection.Invoke(null);
		#endregion
		#region PROGRESS
		List<UploadedFile> _files = new List<UploadedFile>();
		FileUploadProgress _progress;
		#endregion
		#endregion
		#region METHODS
		#region MAIN
		internal void Process()
		{
			try
			{
				using (this._stream = new FileUploadRequestStream(this._wr))
				{
					this._progress = new FileUploadProgress(this._stream.Length);
					using (this._reader = new FileUploadRequestReader(this._stream, this._context.Request.ContentEncoding))
					{
						while (true)
						{
							this.Process_BoundaryAndHeaders();
							if (this._headers.Count == 0)
								break; // final boundary read, quit

							if (this._headers[0].Additional.ContainsKey("filename"))
							{
								this.Process_Upload();
							}
							else
							{
								this.Process_FormField();
							}
						}
					}
				}
				_f_form.SetValue(this._context.Request, this._nvcForm);
			}
			catch (RequestCancelException)
			{
			}
			catch (Exception)
			{
			}
			finally
			{
			}
		}
		private void Process_BoundaryAndHeaders()
		{
			this._headers.Clear();

			string s;
			if (string.IsNullOrEmpty(this._boundary))
			{
				this._boundary = this._reader.ReadLine();
				this._boundaryBytes = this._context.Request.ContentEncoding.GetBytes("\r\n" + this._boundary);
			}
			else
			{
				s = this._reader.ReadLine(); // skip the new line characters right before the boundary
				s = this._reader.ReadLine();
				if (!string.Equals(this._boundary, s))
					return;
				//throw new Exception("Expected boundary but found '" + s + "'.");
			}

			while (!string.IsNullOrEmpty(s = this._reader.ReadLine()))
				this._headers.Add(Header.FromString(s));
		}
		#endregion
		#region CONTENT
		#region GENERIC
		private void Process_Content(Action<byte[], int> action)
		{
			byte[] data = new byte[1024];
			while (this._stream.Position < this._stream.Length)
			{
				if (!this._wr.IsClientConnected())
				{
					throw new RequestCancelException();
				}

				int numBytesRead = this._stream.Peek(data, 0, data.Length);

				int numBytesAvailable = numBytesRead;
				int iBoundary = this.Process_Content_CheckBoundary(data, numBytesRead);
				if (iBoundary != -1)
				{
					numBytesAvailable = iBoundary;
				}

				if (action != null)
					action(data, numBytesAvailable);

				this._stream.Skip(numBytesAvailable);

				if (iBoundary != -1)
					break;
			}

		}
		private int Process_Content_CheckBoundary(byte[] data, int numBytesRead)
		{
			int iBoundary = data.IndexOfPartial(0, numBytesRead, this._boundaryBytes);
			if (iBoundary != -1)
			{
				bool found = true;

				if (iBoundary > numBytesRead - this._boundaryBytes.Length) // did we find a part of the boundary at the end?
				{
					int readBoundaryLength = numBytesRead - iBoundary,
						remainingBoundaryLength = this._boundaryBytes.Length - readBoundaryLength;
					byte[] boundaryCheck = new byte[numBytesRead + remainingBoundaryLength];
					int numBytesReadBoundaryCheck = this._stream.Peek(boundaryCheck, 0, boundaryCheck.Length);

					for (int i = 0; i < remainingBoundaryLength; ++i)
					{
						if (boundaryCheck[numBytesRead + i] != this._boundaryBytes[i + readBoundaryLength])
						{
							found = false;
							break;
						}
					}
				}
				if (found)
					return iBoundary;
			}
			return -1;
		}
		#endregion
		#region SPECIFIC
		private void Process_Upload()
		{
			long fileSize = 0;
			if (!string.IsNullOrEmpty(this._token)) // assuming a single-file is uploaded as part of an AJAX submission
			{
				fileSize =
					this._stream.Length - this._stream.Position // remaining bytes
					//+ this._bufferedData.Length // including the buffered data (left overs from headers)
					//- this._context.Request.ContentEncoding.GetBytes("Content-Disposition: form-data; name=\"submit\"\r\n").Length
					//    // excluding the fin
					- this._context.Request.ContentEncoding.GetByteCount("\r\n")
					- this._boundaryBytes.Length
					- this._context.Request.ContentEncoding.GetByteCount("--"); // excluding the final boundary and the leading dashes
			}
			else
			{
				fileSize = -1; // we don't know the size because we don't know how much more stuff is being sent
			}

			string fileName = "file", uniqueId = "fileUpload";
			foreach (Header h in this._headers)
			{
				if (h.Name.Equals("Content-Disposition"))
				{
					fileName = h.Additional["filename"];
					uniqueId = h.Additional["name"];
					break;
				}
			}

			this._progress.StartFile(fileName, fileSize);

			string filePath = Path.GetTempFileName();
			try
			{
				FileStream fileStream;
				using (fileStream = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.None))
				{
					this.Process_Content(delegate(byte[] data, int numBytes)
					{
						fileStream.Write(data, 0, numBytes);
						this._progress._fileUploaded += numBytes;
					});
				}
			}
			catch
			{
				File.Delete(filePath);
			}

			TimeSpan timeSpan = DateTime.Now.Subtract(this._progress.FileStartDT);

			UploadedFile file = new UploadedFile(uniqueId, fileName, filePath, this._progress._fileUploaded, timeSpan);
			this._files.Add(file);
			this._context.AddFile(file);
		}
		private void Process_FormField()
		{
			StringBuilder sbValue = new StringBuilder();
			this.Process_Content(delegate(byte[] data, int numBytes)
			{
				sbValue.Append(this._context.Request.ContentEncoding.GetString(data, 0, numBytes));
			});

			this._nvcForm.Add(this._headers[0].Additional["name"], sbValue.ToString());
		}
		#endregion
		#endregion
		#endregion
		#endregion
	}
}
